using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;
using System.Collections.Generic;
using Ltf.Remoting.RemoteEvents;
using Ltf.Remoting.TFAccess;
using Ltf.Remoting.TeamFoundation;
using System.Reflection;
using System.Text;

namespace Ltf.Diffs
{
	/// <summary>
	/// Summary description for Class1.
	/// </summary>
	public class DirDiffer
	{
		public enum DifferenceType
		{
			FileDiff,
			FileSame,
			FileAdded,
			SameFileAdded,
			FileMissing,
			FileMatches, // file is in ignore list
			FileConflict, // file is different and known to have other changes in the repository
			FileError, // there was an error reading the file (is it locked?)
			DirDiff,
			DirSame,
			DirAdded,
			DirMissing,
			DirConflict
		}




		[DllImport("imagehlp.dll")]
		public static extern int MapFileAndCheckSumA(string fileName, ref int headerSum, ref int checkSum);

		private IDifRepository repo1, repo2;
		private string path1, path2;
		private string targetPath;
		private XmlDocument result;
		private ArrayList errors;
		private string switches;

		private int _counter = 0;
		private string tempPath;

		private bool checkSumsOnly = false;

		private List<string> ignoreList = new List<string>();
		private List<string> includeList = null;
		private List<string> includeFolderList = new List<string>();
		private bool useIgnoreList = false;
		private bool useIncludeList = false;

		private bool isPatchFileRequired = false;
		private StringBuilder patchFile = new StringBuilder();


		public event EventHandler DiffAnotherTen;

		public event EventHandler ProgressClick;

		public event EventHandler Finished;

		private List<string> renamesOldNames = new List<string>();
		private List<string> renamesNewNames = new List<string>();


		public DirDiffer(IDifRepository repo1, IDifRepository repo2, string path1, string path2, string targetPath, List<string> ignoreList)
		{
			this.repo1 = repo1;
			this.repo2 = repo2;
			this.path1 = path1;
			this.path2 = path2;

			this.targetPath = targetPath;

			tempPath = ServerConnections.GetConnectionForPath(path1).Settings.TempPath;
			this.ignoreList = ignoreList;
			useIgnoreList = true;
			checkSumsOnly = true;
		}

		public DirDiffer(IDifRepository repo1, IDifRepository repo2, string path1, string path2, string targetPath, bool ignoreTabExpansion, bool ignoreSpaceChanges,
							bool ignoreWhiteSpace, bool ignoreBlankLines, bool ignoreCase)
			: this(repo1, repo2, path1, path2, targetPath, ignoreTabExpansion, ignoreSpaceChanges, ignoreWhiteSpace, ignoreBlankLines, ignoreCase, null)
		{

		}
		public DirDiffer(IDifRepository repo1, IDifRepository repo2, string path1, string path2, string targetPath, bool ignoreTabExpansion, bool ignoreSpaceChanges,
									bool ignoreWhiteSpace, bool ignoreBlankLines, bool ignoreCase, List<string> includeList)
		{
			this.repo1 = repo1;
			this.repo2 = repo2;
			this.path1 = path1;
			this.path2 = path2;

			this.targetPath = targetPath;

			switches = (ignoreTabExpansion ? " -E" : "") + (ignoreSpaceChanges ? " -b" : "") + (ignoreWhiteSpace ? " -w" : "")
				+ (ignoreBlankLines ? " -B" : "") + (ignoreCase ? " -i" : "");

			tempPath = ServerConnections.GetConnectionForPath(path1).Settings.TempPath;
			useIgnoreList = false;
			checkSumsOnly = false;

			this.includeList = includeList;
			useIncludeList = (includeList != null && includeList.Count > 0);
			includeFolderList = new List<string>();
			if (useIncludeList)
			{
				foreach (string file in includeList)
					RecurseAddIncludePath(file);
			}
		}

		private void RecurseAddIncludePath(string path)
		{
			string folder = Path.GetDirectoryName(path).Replace("\\","/");
			if (!includeFolderList.Contains(folder))
				includeFolderList.Add(folder);
			if (folder != "$")
				RecurseAddIncludePath(folder);
		}

		public void SetRenameOldNames(string[] oldNames)
		{
			renamesOldNames = new List<string>(oldNames);
		}

		public void SetRenameNewNames(string[] newNames)
		{
			renamesNewNames = new List<string>(newNames);
		}

		public bool Cancelled;

		public void BeginDiffs()
		{
			Cancelled = false;
			isPatchFileRequired = false;
			GoDiffs(useIgnoreList);
			if (Finished != null)
				Finished(this, new EventArgs());
		}

		public void GeneratePatchFile()
		{
			Cancelled = false;
			isPatchFileRequired = true;
			GoDiffs(false);
			if (Finished != null)
				Finished(this, new EventArgs());
		}

		public XmlDocument GoDiffs(bool useIgnoreList)
		{
			errors = new ArrayList();
			if (!repo1.FolderExists(path1))
			{
				errors.Add("No such directory -" + path1);
				result = null;
				return result;
			}
			if (!repo2.FolderExists(path2))
			{
				errors.Add("No such directory -" + path2);
				result = null;
				return result;
			}

			// start the diffing!
			result = new XmlDocument();
			result.LoadXml(string.Format("<DiffResults Path1='{0}' Path2='{1}' />", path1, path2));
			int NodeIndex = 1;
			bool cancel = false;
			RecurseDiff(path1, result.FirstChild, useIgnoreList, ref NodeIndex, ref cancel);
			Cancelled = cancel;
			return result;
		}


		private bool RecurseDiff(String path, XmlNode ResultsNode, bool useIgnoreList, ref int nodeIndex, ref bool cancel)
		{
			bool result = false;
			bool is3Way = ((targetPath != null) && (targetPath != string.Empty));
			List<string> processedFiles = new List<string>();
			TFServerConnection connection = ServerConnections.GetConnectionForPath(path);
			List<string> pathList = new List<string>();
			foreach (string fn in repo1.GetFolderItems(path, DifItemType.File)) // something here to include all bp6 source files
			{
				string includePath = fn;
				if (renamesOldNames.Contains(includePath))
					includePath = renamesNewNames[renamesOldNames.IndexOf(includePath)];

				if (!useIncludeList || includeList.Contains(includePath))
				{
					Console.WriteLine(string.Format("Path In List = {0}", fn));
					string processedName = Path.GetFileName(fn);
					if (connection.Settings.IsFilePathIncluded(fn))
					{
						bool isRenamed = false;
						string fpn = fn.Substring(path1.Length);
						string dfn = path2 + fpn;
						if (renamesOldNames.Contains(path1 + fpn))
						{
							dfn = renamesNewNames[renamesOldNames.IndexOf(path1 + fpn)];
							isRenamed = true;
							processedName = Path.GetFileName(dfn);
						}
						DifferenceType diffType;

						if (repo2.FileExists(dfn))
						{
							if (!useIgnoreList || !ignoreList.Contains(fn))
							{
								if (is3Way || isPatchFileRequired || !checkSumsOnly)
									diffType = FullFileDiff(ref result, fn, dfn, is3Way);
								else
									diffType = GetDiffTypeFromChecksums(ref result, fn, dfn);
							}
							else
								diffType = DifferenceType.FileMatches;
						}
						else
						{
							OneFileDiff(fn, dfn, false, is3Way);
							diffType = DifferenceType.FileMissing;
							result = true;
						}

						if (diffType == DifferenceType.FileDiff && repo2.LastItemHasPotentialConflict)
							diffType = DifferenceType.FileConflict;

						string nodeText = fpn;
						if (isRenamed)
							nodeText = string.Format("{0} [{1}]", dfn, Path.GetFileName(fpn));

						nodeIndex = AddFileNode(ResultsNode, nodeIndex, nodeText, diffType, repo1.GetVersionQualifiedPath(fn), repo2.GetVersionQualifiedPath(dfn));
						if (ProgressClick != null)
						{
							ProgressIndicatorEventArgs ea = new ProgressIndicatorEventArgs(fpn);
							ProgressClick(this, ea);
							cancel = ea.Cancel;
							if (cancel)
								return result;
						}
						if (_counter++ == 10)
						{
							_counter = 0;
							if (DiffAnotherTen != null) DiffAnotherTen(this, new EventArgs());
						}

					}
					processedFiles.Add(processedName);
				}
			}
			string afpn = path.Substring(path1.Length);
			string adfn = path2 + afpn;
			if (!useIncludeList && repo2.FolderExists(adfn))
			{
				// find all the files in the target repository (repo2) that haven't been processed
				// ei. they are new files, not in the source repository (repo1)
				foreach (string newFile in repo2.GetFolderItems(adfn, DifItemType.File))
				{
					if (!processedFiles.Contains(Path.GetFileName(newFile)))
					{
						DifferenceType diffType = DifferenceType.FileAdded;
						// OneFileDiff has the (indirect) side effect of writing to the accumalating
						// patch file, if there is one.
						DifferenceType targetdiffType = OneFileDiff(path1, newFile, true, is3Way);
						if (is3Way && targetdiffType == DifferenceType.FileSame)
							diffType = DifferenceType.SameFileAdded;

						result = true;
						string fpn = Path.GetFileName(newFile);
						if (newFile.StartsWith(path2))
							fpn = newFile.Substring(path2.Length);
						if (newFile.StartsWith(path1))
							fpn = newFile.Substring(path1.Length);

						nodeIndex = AddFileNode(ResultsNode, nodeIndex, fpn, diffType,string.Empty, newFile);
					}
				}
			}
			List<string> processedDirs = new List<string>();
			foreach (string fn in repo1.GetFolderItems(path, DifItemType.Folder)) //something here to exlude .svn files
			{
				if (!useIncludeList || includeFolderList.Contains(fn))
				{
					string folder = Path.GetDirectoryName(fn);

					if (connection.Settings.IsFolderPathIncluded(fn))
					{
						string fpn = fn.Substring(path1.Length);
						string dfn = path2 + fpn;
						DifferenceType diffType;

						if (repo2.FolderExists(dfn))
						{
							diffType = DifferenceType.DirDiff;
						}
						else
						{
							diffType = DifferenceType.DirMissing;
							result = true;
						}

						XmlNode nextnode = ResultsNode.AppendChild(ResultsNode.OwnerDocument.CreateElement("DiffPath"));
						bool diff = RecurseDiff(fn, nextnode, useIgnoreList, ref nodeIndex, ref cancel);
						if (diffType == DifferenceType.DirDiff && !diff)
							diffType = DifferenceType.DirSame;
						else if (repo2.LastItemHasPotentialConflict)
							diffType = DifferenceType.DirConflict;
						nodeIndex = AddDirectoryNode(ResultsNode, nextnode, nodeIndex, fn, folder, diffType);
						if (cancel)
							return result;
					}
					processedDirs.Add(Path.GetFileName(fn));
				}
			}
			if (!useIncludeList && repo2.FolderExists(adfn))
			{
				foreach (string newDir in repo2.GetFolderItems(adfn, DifItemType.Folder))
				{
					if (!processedDirs.Contains(Path.GetFileName(newDir)) && connection.Settings.IsFolderPathIncluded(newDir))
					{
						DifferenceType diffType = DifferenceType.DirAdded;
						result = true;
						XmlNode nextnode = ResultsNode.AppendChild(ResultsNode.OwnerDocument.CreateElement("DiffPath"));
						nodeIndex = AddDirectoryNode(ResultsNode, nextnode, nodeIndex, newDir, Path.GetDirectoryName(newDir), diffType);
					}
				}
			}

			return result;
		}



		private DifferenceType GetDiffTypeFromChecksums(ref bool result, string fn, string dfn)
		{
			DifferenceType diffType;
			int repo1cs = repo1.GetFileChecksum(fn);
			int repo2cs = repo2.GetFileChecksum(dfn);
			if (repo1cs == repo2cs)
			{
				diffType = DifferenceType.FileSame;
			}
			else if (repo1cs == -1)
			{
				diffType = DifferenceType.FileAdded;
			}
			else if (repo2cs == -1)
			{
				diffType = DifferenceType.FileMissing;
			}
			else
			{
				diffType = DifferenceType.FileDiff;
				result = true;
			}
			return diffType;
		}

		public DifferenceType FullFileDiff(ref bool result, string fn, string dfn, bool is3Way)
		{
			bool isDone = false;
			DifferenceType diffType = DifferenceType.FileSame;
			int hs1 = 0, cs1 = 0;
			int hs2 = 0, cs2 = 0;
			try
			{
				string diffPath1 = fn;
				string diffPath2 = dfn;
				// create temp files if files are from tfs.
				if (fn.StartsWith("$"))
				{
					using (Stream s1 = repo1.GetFileItemStream(fn))
					{
						using (StreamReader sr1 = new StreamReader(s1))
						{
							diffPath1 = string.Format("{0}/{1}.1", tempPath, Path.GetFileName(fn));
							File.WriteAllText(diffPath1, sr1.ReadToEnd());
						}
					}
				}
				if (dfn.StartsWith("$"))
				{
					using (Stream s2 = repo2.GetFileItemStream(dfn))
					{
						if (s2 == null)
						{
							diffType = DifferenceType.FileMissing;
							result = true;
							isDone = true;
						}
						else
						{
							using (StreamReader sr2 = new StreamReader(s2))
							{
								diffPath2 = string.Format("{0}/{1}.2", tempPath, Path.GetFileName(dfn));
								File.WriteAllText(diffPath2, sr2.ReadToEnd());
							}
						}
					}
				}


				if (!isDone) // don't bother if repo2 file does not exist
				{
					if (!isPatchFileRequired && !is3Way)  // if no patch or 3 way is required, a quicker checksum diff can be done
					{
						int fh1 = MapFileAndCheckSumA(diffPath1, ref hs1, ref cs1);

						int fh2 = MapFileAndCheckSumA(diffPath2, ref hs2, ref cs2);
					}
					if (isPatchFileRequired || is3Way || cs1 != cs2) // if the checksums don't match then the files are different
						// do a proper diff if a patchfile or 3 way is needed, of if files are different.
						diffType = GetDiffTypeFromGnu(ref result, diffPath1, diffPath2, fn, dfn, is3Way);
					else
						diffType = DifferenceType.FileSame;
				}

				// remove temp files (if used)
				try
				{
					if (diffPath1.StartsWith(tempPath))
						File.Delete(diffPath1);
				}
				catch { /* just ignore */ }
				try
				{
					if (diffPath2.StartsWith(tempPath))
						File.Delete(diffPath2);
				}
				catch { /* just ignore */ }



			}
			catch
			{
				diffType = DifferenceType.FileError;
			}
			return diffType;
		}

		private DifferenceType OneFileDiff(string fn, string dfn, bool isAdded, bool is3Way)
		{
			DifferenceType diffType = DifferenceType.FileSame;
			// this is all about the side effects of calling the GetDiffTypeFromGnu method
			// which will write to the patch file if required.
			IDifRepository repo;
			string testFile;
			if (!isAdded)
			{
				repo = repo1;
				testFile = fn;
			}
			else
			{
				repo = repo2;
				testFile = dfn;
			}
			try
			{
				using (Stream s1 = repo.GetFileItemStream(testFile))
				{
					using (StreamReader sr1 = new StreamReader(s1))
					{
						string tempfile1 = string.Format("{0}/{1}.1", tempPath, Path.GetFileName(fn));
						File.WriteAllText(tempfile1, sr1.ReadToEnd());
						string tempfile2 = Path.GetTempFileName();
						bool result = true;
						if (isAdded)
							diffType = GetDiffTypeFromGnu(ref result, path2, path1, tempfile2, tempfile1, dfn, fn, is3Way, true);
						else
							diffType = GetDiffTypeFromGnu(ref result, path1, path2, tempfile1, tempfile2, fn, dfn, is3Way, true);
						try
						{
							File.Delete(tempfile1);
							File.Delete(tempfile2);
						}
						catch { /* just ignore */ }
					}

				}
			}
			catch { /* fail silently */ }

			return diffType;
		}

		private DifferenceType GetDiffTypeFromGnu(ref bool result, string file1, string file2, string patchFileName1, string patchFileName2, bool is3Way)
		{
			return GetDiffTypeFromGnu(ref result, path1, path2, file1, file2, patchFileName1, patchFileName2, is3Way, false);
		}

		private DifferenceType GetDiffTypeFromGnu(ref bool result, string path1, string path2, string file1, string file2, string patchFileName1, string patchFileName2, bool is3Way, bool isFromOneWay)
		{
			string args = string.Empty;
			string description = string.Empty;
			if (is3Way)
			{
				string localName = (targetPath + patchFileName1.Substring(path1.Length)).Replace("/", "\\");


				if (!File.Exists(localName) && renamesOldNames.Contains(patchFileName1))
				{
					string newLocal = Path.Combine(Path.GetDirectoryName(localName), Path.GetFileName(renamesNewNames[renamesOldNames.IndexOf(patchFileName1)]));
					if (File.Exists(newLocal))
						localName = newLocal;

				}
				string shortLocal = GetShortPath(localName);
				string shortBase = GetShortPath(file1);
				string shortServer = GetShortPath(file2);

				args = "\"" + shortLocal + "\" \"" + shortBase + "\" \"" + shortServer + "\" " + switches;
				description = "\"" + localName + "\" \"" + patchFileName1 + "\" \"" + patchFileName2 + "\" " + switches;
			}
			else
			{
				args = "\"" + file1 + "\" \"" + file2 + "\" " + switches;
				if (isPatchFileRequired)
					args += " -u0 -aN";
				description = "\"" + patchFileName1 + "\" \"" + patchFileName2 + "\" " + switches;
			}

			DifferenceType diffType;

			// quick exit if the file text matches
			if (!is3Way && !isPatchFileRequired)
			{
				if (File.ReadAllText(file1) == File.ReadAllText(file2))
				{
					diffType = DifferenceType.FileSame;
					return diffType;
				}
			}

			Process diff = new Process();

			// they failed the checksum test, but its worth checking with diff as case/blanks etc aren't handled by cs.
			ProcessStartInfo psi = null;
			if (!is3Way)
				psi = new ProcessStartInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\gnu\\diff.exe", args);
			else
				psi = new ProcessStartInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\gnu\\diff3.exe", args);

			psi.RedirectStandardOutput = true;
			psi.RedirectStandardError = true;
			psi.UseShellExecute = false;
			diff.StartInfo = psi;
			diff.StartInfo.CreateNoWindow = true;
			try
			{

				diff.Start();
				string diffOutput = diff.StandardOutput.ReadToEnd();
				int difflen = diffOutput.Length;
				string errors = diff.StandardError.ReadToEnd();
				if (difflen > 0)
				{
					diffType = DifferenceType.FileDiff;
					if (!isFromOneWay && (isPatchFileRequired || is3Way))
					{
						diffOutput = diffOutput.Replace(file1, patchFileName1);
						diffOutput = diffOutput.Replace(file2, patchFileName2);
						patchFile.AppendLine("diff " + description);
						patchFile.Append(diffOutput);
					}
				}
				else
					diffType = DifferenceType.FileSame;
			}
			catch
			{
				diffType = DifferenceType.FileDiff;
				result = true;
			}
			return diffType;
		}

		private int AddDirectoryNode(XmlNode ResultsNode, XmlNode nextnode, int nodeIndex, string fn, string folder, DifferenceType diffType)
		{
			XmlAttribute fna = ResultsNode.OwnerDocument.CreateAttribute("DirName");
			fna.Value = Path.GetFileName(fn);
			nextnode.Attributes.Append(fna);
			fna = ResultsNode.OwnerDocument.CreateAttribute("DirFullName");
			fna.Value = fn;
			nextnode.Attributes.Append(fna);
			fna = ResultsNode.OwnerDocument.CreateAttribute("NodeIndex");
			fna.Value = (nodeIndex++).ToString();
			nextnode.Attributes.Append(fna);
			fna = ResultsNode.OwnerDocument.CreateAttribute("DirDiff");
			fna.Value = diffType.ToString();
			nextnode.Attributes.Append(fna);
			return nodeIndex;
		}

		private static int AddFileNode(XmlNode ResultsNode, int NodeIndex, string fpn, DifferenceType diffType, string fromFilePath, string toFilePath)
		{
			string fn = Path.GetFileName(fpn);
			XmlNode fd = ResultsNode.OwnerDocument.CreateElement(diffType.ToString());
			
			XmlAttribute fna = ResultsNode.OwnerDocument.CreateAttribute("FileName");
			fna.Value = fn;
			fd.Attributes.Append(fna);
			
			fna = ResultsNode.OwnerDocument.CreateAttribute("FileFullName");
			fna.Value = fpn;
			fd.Attributes.Append(fna);
			
			fna = ResultsNode.OwnerDocument.CreateAttribute("NodeIndex");
			fna.Value = (NodeIndex++).ToString();
			fd.Attributes.Append(fna);

			fna = ResultsNode.OwnerDocument.CreateAttribute("FromFilePath");
			fna.Value = fromFilePath;
			fd.Attributes.Append(fna);

			fna = ResultsNode.OwnerDocument.CreateAttribute("ToFilePath");
			fna.Value = toFilePath;
			fd.Attributes.Append(fna);

			ResultsNode.AppendChild(fd);
			return NodeIndex;
		}

		public XmlDocument Results
		{
			get { return result; }
		}

		public ArrayList Errors
		{
			get { return errors; }
		}

		public String PatchFile
		{
			get { return patchFile.ToString(); }
		}

		[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
		private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPTStr)] string path,
			[MarshalAs(UnmanagedType.LPTStr)] StringBuilder shortPath, int shortPathLength);

		private static string GetShortPath(string longPath)
		{
			StringBuilder shortPath = new StringBuilder(255);
			GetShortPathName(longPath, shortPath, shortPath.Capacity);
			return shortPath.ToString();
		}

	}
}